package com.xixi.canal.client.support;

import com.xixi.canal.client.util.Assert;
import com.xixi.canal.client.util.StringUtil;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import java.io.BufferedReader;
import java.io.File;
import java.io.InputStreamReader;
import java.net.MalformedURLException;
import java.net.URL;
import java.net.URLClassLoader;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.util.Enumeration;
import java.util.HashMap;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.ConcurrentMap;
import java.util.regex.Pattern;

public class ExtensionLoader<T> {

    private static  final Logger logger = LoggerFactory.getLogger(ExtensionLoader.class);

    private static final String DEFAULT_CLASSLOADER_POLICY = "internal";

    private static final String CANAL_DIR = "META-INF/canal/";

    private static final String SERVERS_DIR = "META-INF/services/";

    private static final Pattern NAME_SEPARATOR = Pattern
            .compile("\\s*[,]+\\s*");

    private static final ConcurrentHashMap<Class<?>, ExtensionLoader<?>> EXTENSION_LOADERS =
            new ConcurrentHashMap<>();

    private static final ConcurrentMap<String, Object> EXTENSION_KEY_INSTANCE  = new ConcurrentHashMap<>();

    private final Class<?> type;

    private final String classLoaderPolicy;

    private String defaultCacheName;

    private final ConcurrentHashMap<String, Holder<T>> instanceCache = new ConcurrentHashMap<>();

    private final Holder<Map<String, Class<?>>> classesHolder = new Holder<>();

    public static <T> ExtensionLoader<T> getExtensionLoader(Class<T> type) {
        return getExtensionLoader(type, DEFAULT_CLASSLOADER_POLICY);
    }


    public static <T> ExtensionLoader<T> getExtensionLoader(Class<T> type, String classLoaderPolicy) {

        Assert.notNull(type, "Extension type must not be null");
        Assert.notInterface(type, "Extension type(" + type + ") must be interface");
        Assert.withAnnotation(type, SPI.class, "Extension type(" + type + ") is not extension, because WITHOUT @"
                + SPI.class.getSimpleName() + " Annotation");

        ExtensionLoader<T> loader = (ExtensionLoader<T>) EXTENSION_LOADERS.get(type);

        if (null == loader) {
            EXTENSION_LOADERS.putIfAbsent(type, new ExtensionLoader<T>(type, classLoaderPolicy));
            loader = (ExtensionLoader<T>) EXTENSION_LOADERS.get(type);
        }

        return loader;
    }

    public ExtensionLoader(Class<?> type, String classLoaderPolicy) {
        this.type = type;
        this.classLoaderPolicy = classLoaderPolicy;
    }

    public T getExtension(String name, String key) {

        Assert.notNull(name, "Extension name must not be null");

        String extKey = buildExtensionKey(name, key);

        Holder<T> holder = instanceCache.get(extKey);

        if (null == holder) {
            instanceCache.putIfAbsent(extKey, new Holder<>());
            holder = instanceCache.get(extKey);
        }

        T instance = holder.get();

        if (null == instance) {
            synchronized (holder) {
                instance = holder.get();
                if (null == instance) {
                    instance = createExtension(name, key);
                    holder.set(instance);
                }
            }
        }
        return instance;
    }

    private T createExtension(String name, String key) {

        Class<?> clazz = findExtensionClass(name);

        Assert.notNull(clazz, "Extension instance(name: " + name + ", class: " + type
                + ")  could not be instantiated: class could not be found");

        String extKey = buildExtensionKey(name, key);

        try {
            T instance = (T)EXTENSION_KEY_INSTANCE.get(extKey);
            if (null == instance) {
                EXTENSION_KEY_INSTANCE.putIfAbsent(extKey, clazz.newInstance());
                instance = (T)EXTENSION_KEY_INSTANCE.get(extKey);
            }
            return instance;
        } catch (Throwable t) {
            throw new IllegalStateException("Extension instance(name: " + name + ", class: " + type
                    + ")  could not be instantiated: " + t.getMessage(),
                    t);
        }
    }

    private Class<?> findExtensionClass(String name) {

        Map<String, Class<?>> classMap = classesHolder.get();

        if (null == classMap) {
            synchronized (classesHolder) {
                classMap = classesHolder.get();
                if (null == classMap) {
                    classMap = loadExtensionClasses();
                    classesHolder.set(classMap);
                }
            }
        }
        return classMap.get(name);
    }

    private Map<String, Class<?>> loadExtensionClasses() {

        Map<String, Class<?>> classes = new HashMap<String, Class<?>>();
        // load default
        final SPI defaultAnnotation = type.getAnnotation(SPI.class);
        if (null != defaultAnnotation) {
            defaultCacheName = defaultAnnotation.value();
        }

        String pluginJarDir = File.separator + this.getJarDirectoryPath() + File.separator + "plugin";

        File externalJarDir = new File(pluginJarDir);
        if (!externalJarDir.exists()) {
            pluginJarDir = File.separator + this.getJarDirectoryPath() + File.separator + "canal-client"
                    + File.separator + "plugin";
            externalJarDir = new File(pluginJarDir);
        }
        logger.info("extension classpath dir: " + externalJarDir.getAbsolutePath());


        File[] files = externalJarDir.listFiles((dir, name) -> name.endsWith(".jar"));

        if (null != files) {
            for (File file : files) {

                try {
                    URL url = file.toURI().toURL();

                    ClassLoader parent = Thread.currentThread().getContextClassLoader();

                    URLClassLoader localClassLoader = null;

                    if (!StringUtil.hasText(classLoaderPolicy)
                            || DEFAULT_CLASSLOADER_POLICY.equalsIgnoreCase(classLoaderPolicy)) {
                        localClassLoader = new InternalURLClassExtensionLoader(new URL[] {url});
                    } else {
                        localClassLoader = new URLClassLoader(new URL[]{ url }, parent);
                    }

                    loadFile(classes, CANAL_DIR, localClassLoader);

                    loadFile(classes, SERVERS_DIR, localClassLoader);

                } catch (MalformedURLException e) {
                    throw new RuntimeException("Load extension jar[" + file.getAbsolutePath() +"] failed:", e);
                }
            }
        }
        return classes;
    }

    private void loadFile(Map<String, Class<?>> classes, String dir, URLClassLoader classLoader) {

        String fileName = dir + type.getName();
        try {
            Enumeration<URL> urls = null;
            if (null != classLoader) {
                urls = classLoader.getResources(fileName);
            } else {
                urls = ClassLoader.getSystemResources(fileName);
            }

            if (null == urls) return;

            while (urls.hasMoreElements()) {
                URL url = urls.nextElement();
                try (BufferedReader reader = new BufferedReader(
                        new InputStreamReader(url.openStream(), "utf-8"))){
                    String line = null;
                    while ((line = reader.readLine()) != null) {
                        final int ci = line.indexOf('#');
                        if (ci >= 0) line = line.substring(0, ci);
                        line = line.trim();

                        if (!StringUtil.hasText(line)) continue;

                        try {
                            int i = line.indexOf("=");
                            String name = null;
                            if (i > 0) {
                                name = line.substring(0, i).trim();
                                line = line.substring(i + 1).trim();
                            }

                            if (!StringUtil.hasText(line)) continue;

                            Class<?> clazz = classLoader.loadClass(line);

                            if (!type.isAssignableFrom(clazz)) {
                                throw new IllegalStateException(
                                        "Error when load extension class(interface: " + type
                                                + ", class line: " + clazz.getName()
                                                + "), class " + clazz.getName()
                                                + "is not subtype of interface.");
                            }

                            try {
                                clazz.getConstructor(type);
                            } catch (NoSuchMethodException e) {
                                clazz.getConstructor();
                                String[] names = NAME_SEPARATOR.split(name);

                                if (null == name || names.length == 0) continue;

                                for (String s : names) {

                                    Class<?> c = classes.get(s);
                                    if (null == c) {
                                        classes.put(s, clazz);
                                    } else if (c != clazz) {
                                        throw new IllegalStateException(
                                                "Duplicate extension " + type.getName() + " name "
                                                        + s + " on "
                                                        + c.getName() + " and "
                                                        + clazz.getName());

                                    }
                                }
                            }
                        } catch (Throwable t) {
                            logger.error(
                                    "Failed to load extension class(interface: " + type + ", class line: "
                                            + line + ") in " + url
                                            + ", cause: "
                                            + t.getMessage(),
                                    t);
                        }
                    }

                }
            }


        } catch (Throwable t) {
            logger.error(
                    "Exception when load extension class(interface: " + type + ", description file: " + fileName + ").",
                    t);
        }

    }

    private String getJarDirectoryPath() {

        URL url = Thread.currentThread().getContextClassLoader().getResource("");
        String urlPath = null;
        if (url != null) {
            urlPath = url.toString();
        } else {
            urlPath = new File("").getAbsolutePath();
        }
        Path path = Paths.get(StringUtil.getJarPathDir(urlPath)).getParent();

        if (null != path) return path.toString();

        return null;
    }

    private String buildExtensionKey(String name, String key) {
        return name + "-" + StringUtil.trimToEmpty(key);
    }

    private static class Holder<T> {

        private volatile T value;

        private void set(T value) {
            this.value = value;
        }

        private T get() {
            return value;
        }
    }
}
